<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>鼠标选择立方体</title>
  <style>
    body {
      margin: 0;
      overflow: hidden
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      uniform mat4 u_PvMatrix;
      uniform mat4 u_ModelMatrix;
      varying vec4 v_Color;
      void main(){
        gl_Position = u_PvMatrix*u_ModelMatrix*a_Position;
        v_Color=a_Color;
      }
  </script>
  <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      uniform float u_Time;
      varying vec4 v_Color;
      void main(){
        float r=(sin(u_Time/200.0)+1.0)/2.0;
        gl_FragColor=vec4(v_Color.r*r,v_Color.g*(1.0-r),v_Color.ba);
      }
  </script>
  <script type="module">
    import { initShaders, createProgram } from '../jsm/Utils.js';
    import {
      Matrix4, PerspectiveCamera, Vector3, Quaternion, Ray
    } from 'https://unpkg.com/three/build/three.module.js';
    import OrbitControls from './jsm/OrbitControls.js'
    import Mat from './jsm/Mat.js'
    import Geo from './jsm/Geo.js'
    import Obj3D from './jsm/Obj3D.js'
    import Scene from './jsm/Scene.js'

    const canvas = document.getElementById('canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const gl = canvas.getContext('webgl');

    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;
    const program = createProgram(gl, vsSource, fsSource);
    gl.clearColor(0, 0, 0, 1);
    gl.enable(gl.DEPTH_TEST);



    /* 透视相机 */
    const eye = new Vector3(2, 3, 5)
    const target = new Vector3(0, 0, 0)
    const up = new Vector3(0, 1, 0)
    const [fov, aspect, near, far] = [
      45,
      canvas.width / canvas.height,
      1,
      20
    ]
    const camera = new PerspectiveCamera(fov, aspect, near, far)
    camera.position.copy(eye)

    /* 实例化轨道控制器 */
    const orbit = new OrbitControls({
      camera, target,
      dom: canvas,
    })
    orbit.update()

    // 投影视图矩阵
    const pvMatrix = new Matrix4()
    // 模型矩阵
    const modelMatrix = new Matrix4()
    // 插值
    let time = 0
    // 弧度
    let ang = 0
    // 选择状态
    let selected = false
    // 顶点集合
    const vertices = new Float32Array([
      1, 1, 1,
      -1, 1, 1,
      -1, -1, 1,
      1, -1, 1,
      1, -1, -1,
      1, 1, -1,
      -1, 1, -1,
      -1, -1, -1,
    ])
    // 顶点索引集合
    const indexes = new Uint8Array([
      0, 1, 2, 0, 2, 3,    // front
      0, 3, 4, 0, 4, 5,    // right
      0, 5, 6, 0, 6, 1,    // up
      1, 6, 7, 1, 7, 2,    // left
      7, 4, 3, 7, 3, 2,    // down
      4, 7, 6, 4, 6, 5     // back
    ])

    /* 取消右击菜单的显示 */
    canvas.addEventListener('contextmenu', event => {
      event.preventDefault()
    })
    /* 指针按下时，设置拖拽起始位，获取轨道控制器状态。 */
    canvas.addEventListener('pointerdown', event => {
      orbit.pointerdown(event)
    })
    /* 指针移动时，若控制器处于平移状态，平移相机；若控制器处于旋转状态，旋转相机。 */
    canvas.addEventListener('pointermove', event => {
      orbit.pointermove(event)
      pvMatrix.copy(orbit.getPvMatrix())
      selectObj(event)
    })
    /* 指针抬起 */
    canvas.addEventListener('pointerup', event => {
      orbit.pointerup(event)
    })
    /* 滚轮事件 */
    canvas.addEventListener('wheel', event => {
      orbit.wheel(event)
      pvMatrix.copy(orbit.getPvMatrix())
    })


    const scene = new Scene({ gl })
    const mat = new Mat({
      program,
      data: {
        u_Time: {
          value: 0,
          type: 'uniform1f',
        },
        u_PvMatrix: {
          value: orbit.getPvMatrix().elements,
          type: 'uniformMatrix4fv',
        },
        u_ModelMatrix: {
          value: new Matrix4().elements,
          type: 'uniformMatrix4fv',
        },
      }
    })
    const geo = new Geo({
      data: {
        a_Position: {
          array: vertices,
          size: 3
        },
        a_Color: {
          array: new Float32Array([
            1, 0, 0,
            0, 1, 0,
            0, 0, 1,
            1, 1, 0,
            0, 1, 1,
            1, 0, 1,
            1, 1, 1,
            0, 0, 0
          ]),
          size: 3
        },
      },
      index: {
        array: indexes
      }
    })
    const obj = new Obj3D({ geo, mat })
    scene.add(obj)

    // 选择三维对象
    function selectObj(event) {
      // 鼠标世界位
      const mp = worldPos(event)
      // 射线
      const ray = new Ray(camera.position).lookAt(mp)
      // 选择状态
      selected = false
      // 遍历三角形
      for (let i = 0; i < indexes.length; i += 3) {
        // 三角形
        const triangle = [
          getVector3(indexes[i]),
          getVector3(indexes[i + 1]),
          getVector3(indexes[i + 2]),
        ]
        // 射线与三角形的交点
        const interPos = intersectTriangle(ray, triangle)
        // 更新选择状态
        if (interPos) {
          selected = true
          break
        }
      }
      console.log('selected', selected);
    }

    // 射线与三角形的交点
    function intersectTriangle(ray, triangle) {
      const { origin: E, direction: v } = ray
      const [A, B, C] = triangle
      // 三角形的法线
      const AB = new Vector3().subVectors(B, A)
      const BC = new Vector3().subVectors(C, B)
      const n = new Vector3().crossVectors(AB, BC)

      // 射线和平面的交点
      const M = v.clone().multiplyScalar(
        A.clone().sub(E).dot(n) / v.clone().dot(n)
      ).add(E)

      // 判断点M是否在三角形中
      let bool = true
      for (let i = 0; i < 3; i++) {
        const j = (i + 1) % 3
        const [a, b] = [triangle[i], triangle[j]]
        const ma = a.clone().sub(M)
        const ab = b.clone().sub(a)
        const d = ma.clone().cross(ab)
        const len = d.dot(n)
        if (len < 0) {
          bool = false
          break
        }
      }
      if (bool) {
        return M
      } else {
        return null
      }
    }

    // 基于顶点索引获取顶点数据
    function getVector3(j) {
      const i = j * 3
      return new Vector3(
        vertices[i],
        vertices[i + 1],
        vertices[i + 2],
      ).applyMatrix4(modelMatrix)
    }

    // 获取鼠标世界位
    function worldPos({ clientX, clientY }) {
      const [hw, hh] = [canvas.width / 2, canvas.height / 2]
      // 裁剪空间位
      const cp = new Vector3(
        (clientX - hw) / hw,
        -(clientY - hh) / hh,
        0
      )
      // 鼠标在世界坐标系中的位置
      return cp.applyMatrix4(
        pvMatrix.clone().invert()
      )
    }

    !(function ani() {
      scene.setUniform(
        'u_PvMatrix',
        orbit.getPvMatrix().elements
      )
      if (selected) {
        time += 20
        ang += 0.05
        mat.setData(
          'u_Time',
          {
            value: time
          }
        )
        mat.setData(
          'u_ModelMatrix',
          {
            value: new Matrix4().makeRotationY(ang).elements
          }
        )
      }

      scene.draw()
      requestAnimationFrame(ani)
    })()


  </script>
</body>

</html>